import path from "path";
import test from "ava";
import { copy, outputFile } from "fs-extra";
import { stub } from "sinon";
import { temporaryDirectory } from "tempy";
import getPlugins from "../../lib/plugins/index.js";

// Save the current working directory
const cwd = process.cwd();

test.beforeEach((t) => {
  // Stub the logger functions
  t.context.log = stub();
  t.context.success = stub();
  t.context.logger = { log: t.context.log, success: t.context.success, scope: () => t.context.logger };
});

test("Export default plugins", async (t) => {
  const plugins = await getPlugins({ cwd, options: {}, logger: t.context.logger }, {});

  // Verify the module returns a function for each plugin
  t.is(typeof plugins.verifyConditions, "function");
  t.is(typeof plugins.analyzeCommits, "function");
  t.is(typeof plugins.verifyRelease, "function");
  t.is(typeof plugins.generateNotes, "function");
  t.is(typeof plugins.prepare, "function");
  t.is(typeof plugins.publish, "function");
  t.is(typeof plugins.success, "function");
  t.is(typeof plugins.fail, "function");
});

test("Export plugins based on steps config", async (t) => {
  const plugins = await getPlugins(
    {
      cwd,
      logger: t.context.logger,
      options: {
        verifyConditions: ["./test/fixtures/plugin-noop.cjs", { path: "./test/fixtures/plugin-noop.cjs" }],
        generateNotes: "./test/fixtures/plugin-noop.cjs",
        analyzeCommits: { path: "./test/fixtures/plugin-noop.cjs" },
        verifyRelease: () => {},
      },
    },
    {}
  );

  // Verify the module returns a function for each plugin
  t.is(typeof plugins.verifyConditions, "function");
  t.is(typeof plugins.analyzeCommits, "function");
  t.is(typeof plugins.verifyRelease, "function");
  t.is(typeof plugins.generateNotes, "function");
  t.is(typeof plugins.prepare, "function");
  t.is(typeof plugins.publish, "function");
  t.is(typeof plugins.success, "function");
  t.is(typeof plugins.fail, "function");
});

test('Export plugins based on "plugins" config (array)', async (t) => {
  const plugin1 = { verifyConditions: stub(), publish: stub() };
  const plugin2 = { verifyConditions: stub(), verifyRelease: stub() };
  const plugins = await getPlugins(
    { cwd, logger: t.context.logger, options: { plugins: [plugin1, [plugin2, {}]], verifyRelease: () => {} } },
    {}
  );
  await plugins.verifyConditions({ options: {} });
  t.true(plugin1.verifyConditions.calledOnce);
  t.true(plugin2.verifyConditions.calledOnce);

  await plugins.publish({ options: {} });
  t.true(plugin1.publish.calledOnce);

  await plugins.verifyRelease({ options: {} });
  t.true(plugin2.verifyRelease.notCalled);

  // Verify the module returns a function for each plugin
  t.is(typeof plugins.verifyConditions, "function");
  t.is(typeof plugins.analyzeCommits, "function");
  t.is(typeof plugins.verifyRelease, "function");
  t.is(typeof plugins.generateNotes, "function");
  t.is(typeof plugins.prepare, "function");
  t.is(typeof plugins.publish, "function");
  t.is(typeof plugins.success, "function");
  t.is(typeof plugins.fail, "function");
});

test('Export plugins based on "plugins" config (single definition)', async (t) => {
  const plugin1 = { verifyConditions: stub(), publish: stub() };
  const plugins = await getPlugins({ cwd, logger: t.context.logger, options: { plugins: plugin1 } }, {});

  await plugins.verifyConditions({ options: {} });
  t.true(plugin1.verifyConditions.calledOnce);

  await plugins.publish({ options: {} });
  t.true(plugin1.publish.calledOnce);

  // Verify the module returns a function for each plugin
  t.is(typeof plugins.verifyConditions, "function");
  t.is(typeof plugins.analyzeCommits, "function");
  t.is(typeof plugins.verifyRelease, "function");
  t.is(typeof plugins.generateNotes, "function");
  t.is(typeof plugins.prepare, "function");
  t.is(typeof plugins.publish, "function");
  t.is(typeof plugins.success, "function");
  t.is(typeof plugins.fail, "function");
});

test('Merge global options, "plugins" options and step options', async (t) => {
  const plugin1 = [{ verifyConditions: stub(), publish: stub() }, { pluginOpt1: "plugin1" }];
  const plugin2 = [{ verifyConditions: stub() }, { pluginOpt2: "plugin2" }];
  const plugin3 = [stub(), { pluginOpt3: "plugin3" }];
  const plugins = await getPlugins(
    {
      cwd,
      logger: t.context.logger,
      options: { globalOpt: "global", plugins: [plugin1, plugin2], verifyRelease: [plugin3] },
    },
    {}
  );

  await plugins.verifyConditions({ options: {} });
  t.deepEqual(plugin1[0].verifyConditions.args[0][0], { globalOpt: "global", pluginOpt1: "plugin1" });
  t.deepEqual(plugin2[0].verifyConditions.args[0][0], { globalOpt: "global", pluginOpt2: "plugin2" });

  await plugins.publish({ options: {} });
  t.deepEqual(plugin1[0].publish.args[0][0], { globalOpt: "global", pluginOpt1: "plugin1" });

  await plugins.verifyRelease({ options: {} });
  t.deepEqual(plugin3[0].args[0][0], { globalOpt: "global", pluginOpt3: "plugin3" });
});

test('Unknown steps of plugins configured in "plugins" are ignored', async (t) => {
  const plugin1 = { verifyConditions: () => {}, unknown: () => {} };
  const plugins = await getPlugins({ cwd, logger: t.context.logger, options: { plugins: [plugin1] } }, {});

  t.is(typeof plugins.verifyConditions, "function");
  t.is(plugins.unknown, undefined);
});

test("Export plugins loaded from the dependency of a shareable config module", async (t) => {
  const cwd = temporaryDirectory();
  await copy(
    "./test/fixtures/plugin-noop.cjs",
    path.resolve(cwd, "node_modules/shareable-config/node_modules/custom-plugin/index.js")
  );
  await outputFile(path.resolve(cwd, "node_modules/shareable-config/index.js"), "");

  const plugins = await getPlugins(
    {
      cwd,
      logger: t.context.logger,
      options: {
        verifyConditions: ["custom-plugin", { path: "custom-plugin" }],
        generateNotes: "custom-plugin",
        analyzeCommits: { path: "custom-plugin" },
        verifyRelease: () => {},
      },
    },
    { "custom-plugin": "shareable-config" }
  );

  // Verify the module returns a function for each plugin
  t.is(typeof plugins.verifyConditions, "function");
  t.is(typeof plugins.analyzeCommits, "function");
  t.is(typeof plugins.verifyRelease, "function");
  t.is(typeof plugins.generateNotes, "function");
  t.is(typeof plugins.prepare, "function");
  t.is(typeof plugins.publish, "function");
  t.is(typeof plugins.success, "function");
  t.is(typeof plugins.fail, "function");
});

test("Export plugins loaded from the dependency of a shareable config file", async (t) => {
  const cwd = temporaryDirectory();
  await copy("./test/fixtures/plugin-noop.cjs", path.resolve(cwd, "plugin/plugin-noop.cjs"));
  await outputFile(path.resolve(cwd, "shareable-config.js"), "");

  const plugins = await getPlugins(
    {
      cwd,
      logger: t.context.logger,
      options: {
        verifyConditions: ["./plugin/plugin-noop.cjs", { path: "./plugin/plugin-noop.cjs" }],
        generateNotes: "./plugin/plugin-noop.cjs",
        analyzeCommits: { path: "./plugin/plugin-noop.cjs" },
        verifyRelease: () => {},
      },
    },
    { "./plugin/plugin-noop": "./shareable-config.js" }
  );

  // Verify the module returns a function for each plugin
  t.is(typeof plugins.verifyConditions, "function");
  t.is(typeof plugins.analyzeCommits, "function");
  t.is(typeof plugins.verifyRelease, "function");
  t.is(typeof plugins.generateNotes, "function");
  t.is(typeof plugins.prepare, "function");
  t.is(typeof plugins.publish, "function");
  t.is(typeof plugins.success, "function");
  t.is(typeof plugins.fail, "function");
});

test("Use default when only options are passed for a single plugin", async (t) => {
  const analyzeCommits = {};
  const generateNotes = {};
  const publish = {};
  const success = () => {};
  const fail = [() => {}];

  const plugins = await getPlugins(
    {
      cwd,
      logger: t.context.logger,
      options: {
        plugins: ["@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator"],
        analyzeCommits,
        generateNotes,
        publish,
        success,
        fail,
      },
    },
    {}
  );

  // Verify the module returns a function for each plugin
  t.is(typeof plugins.analyzeCommits, "function");
  t.is(typeof plugins.generateNotes, "function");
  t.is(typeof plugins.success, "function");
  t.is(typeof plugins.fail, "function");

  // Verify only the plugins defined as an object with no `path` are set to the default value
  t.falsy(success.path);
  t.falsy(fail.path);
});

test("Merge global options with plugin options", async (t) => {
  const plugins = await getPlugins(
    {
      cwd,
      logger: t.context.logger,
      options: {
        globalOpt: "global",
        otherOpt: "globally-defined",
        verifyRelease: { path: "./test/fixtures/plugin-result-config", localOpt: "local", otherOpt: "locally-defined" },
      },
    },
    {}
  );

  const [result] = await plugins.verifyRelease({ options: {} });

  t.deepEqual(result.pluginConfig, { localOpt: "local", globalOpt: "global", otherOpt: "locally-defined" });
});

test("Throw an error for each invalid plugin configuration", async (t) => {
  const errors = [
    ...(
      await t.throwsAsync(() =>
        getPlugins(
          {
            cwd,
            logger: t.context.logger,
            options: {
              plugins: ["@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator"],
              verifyConditions: 1,
              analyzeCommits: [],
              verifyRelease: [{}],
              generateNotes: [{ path: null }],
            },
          },
          {}
        )
      )
    ).errors,
  ];

  t.is(errors[0].name, "SemanticReleaseError");
  t.is(errors[0].code, "EPLUGINCONF");
  t.is(errors[1].name, "SemanticReleaseError");
  t.is(errors[1].code, "EPLUGINCONF");
  t.is(errors[2].name, "SemanticReleaseError");
  t.is(errors[2].code, "EPLUGINCONF");
  t.is(errors[3].name, "SemanticReleaseError");
  t.is(errors[3].code, "EPLUGINCONF");
});

test('Throw EPLUGINSCONF error if the "plugins" option contains an old plugin definition (returns a function)', async (t) => {
  const errors = [
    ...(
      await t.throwsAsync(() =>
        getPlugins(
          {
            cwd,
            logger: t.context.logger,
            options: { plugins: ["./test/fixtures/multi-plugin.cjs", "./test/fixtures/plugin-noop.cjs", () => {}] },
          },
          {}
        )
      )
    ).errors,
  ];

  t.is(errors[0].name, "SemanticReleaseError");
  t.is(errors[0].code, "EPLUGINSCONF");
  t.is(errors[1].name, "SemanticReleaseError");
  t.is(errors[1].code, "EPLUGINSCONF");
});

test('Throw EPLUGINSCONF error for each invalid definition if the "plugins" option', async (t) => {
  const errors = [
    ...(
      await t.throwsAsync(() =>
        getPlugins({ cwd, logger: t.context.logger, options: { plugins: [1, { path: 1 }, [() => {}, {}, {}]] } }, {})
      )
    ).errors,
  ];

  t.is(errors[0].name, "SemanticReleaseError");
  t.is(errors[0].code, "EPLUGINSCONF");
  t.is(errors[1].name, "SemanticReleaseError");
  t.is(errors[1].code, "EPLUGINSCONF");
  t.is(errors[2].name, "SemanticReleaseError");
  t.is(errors[2].code, "EPLUGINSCONF");
});
